﻿using System;
using System.Collections.Generic;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Shapes;

namespace SpiralClock
{
    /// <summary>
    /// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
    ///
    /// Step 1a) Using this custom control in a XAML file that exists in the current project.
    /// Add this XmlNamespace attribute to the root element of the markup file where it is 
    /// to be used:
    ///
    ///     xmlns:MyNamespace="clr-namespace:SpiralClock"
    ///
    ///
    /// Step 1b) Using this custom control in a XAML file that exists in a different project.
    /// Add this XmlNamespace attribute to the root element of the markup file where it is 
    /// to be used:
    ///
    ///     xmlns:MyNamespace="clr-namespace:SpiralClock;assembly=SpiralClock"
    ///
    /// You will also need to add a project reference from the project where the XAML file lives
    /// to this project and Rebuild to avoid compilation errors:
    ///
    ///     Right click on the target project in the Solution Explorer and
    ///     "Add Reference"->"Projects"->[Browse to and select this project]
    ///
    ///
    /// Step 2)
    /// Go ahead and use your control in the XAML file.
    ///
    ///     <MyNamespace:SpiralItemsPanel/>
    ///
    /// </summary>
    [TemplatePart(Name = "PART_canvas", Type = typeof (Canvas))]
    [TemplatePart(Name = "PART_LayoutRoot", Type = typeof (Grid))]
    [TemplatePart(Name = "PART_now", Type = typeof (Ellipse))]
    public class SpiralItemsControl : Selector
    {
        #region Fields (14) 

        private const double a = 4*r2 - 3*r1;
        private const double b = 2*(r2 - r1)/Math.PI;
        private const double r1 = 22.52;
        private const double r2 = 18.00;
        private const double x0 = 50;
        private const double y0 = 45.5;
        // Using a DependencyProperty as the backing store for HourLabelTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HourLabelStyleProperty =
            DependencyProperty.Register("HourLabelStyle", typeof (Style), typeof (SpiralItemsControl),
                                        new UIPropertyMetadata(null));

        private readonly List<TextBlock> _hourLabels = new List<TextBlock>();

        private readonly List<Point> _hourPositions = new List<Point>();
        private readonly Timer _timer = new Timer();
        private Canvas _canvas;
        private Ellipse _now;

        #endregion Fields 

        #region Constructors (2) 

        static SpiralItemsControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof (SpiralItemsControl),
                                                     new FrameworkPropertyMetadata(typeof (SpiralItemsControl)));
        }

        public SpiralItemsControl()
        {
            LayoutUpdated += SpiralItemsControl_LayoutUpdated;

            _timer.Interval = 20000;
            _timer.Elapsed += TimerElapsed;
            _timer.Start();
        }

        #endregion Constructors 

        #region Properties (1) 

        public Style HourLabelStyle
        {
            get { return (Style) GetValue(HourLabelStyleProperty); }
            set { SetValue(HourLabelStyleProperty, value); }
        }

        #endregion Properties 

        #region Methods (12) 

        // Public Methods (1) 

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            _canvas = Template.FindName("PART_canvas", this) as Canvas;
            _now = Template.FindName("PART_now", this) as Ellipse;

            Initialize();
        }

        // Protected Methods (2) 

        protected override Size ArrangeOverride(Size arrangeBounds)
        {
            base.ArrangeOverride(arrangeBounds);


            ArrangeNow(arrangeBounds);

            TransformGroup trg2 = GetPointTransform(arrangeBounds, 0.93);


            int i = 0;
            foreach (TextBlock textb in _hourLabels)
            {
                Point pos = _hourPositions[i];
                //textb.Height = textb.Width= 5 * scale;
                Point newPos = trg2.Transform(pos);

                textb.Arrange(new Rect(newPos.X - textb.DesiredSize.Width/2,
                                       arrangeBounds.Height - newPos.Y - textb.DesiredSize.Height/2,
                                       textb.DesiredSize.Width, textb.DesiredSize.Height));
                i++;
            }


            return arrangeBounds;
        }

        protected override Size MeasureOverride(Size constraint)
        {
            _now.Measure(constraint);

            foreach (TextBlock text in _hourLabels)
            {
                text.Measure(constraint);
            }
            return base.MeasureOverride(constraint);
        }

        // Private Methods (9) 

        private void ArrangeNow(Size arrangeBounds)
        {
            double angle = CalculateAngle(DateTime.Now);
            Point initPos = CalculatePosition(angle);

            TransformGroup trg = GetPointTransform(arrangeBounds);
            Point newPos = trg.Transform(initPos);


            _now.Arrange(new Rect(newPos.X - _now.DesiredSize.Width/2,
                                 arrangeBounds.Height - newPos.Y - _now.DesiredSize.Height/2, _now.DesiredSize.Width,
                                 _now.DesiredSize.Height));
        }

        private double CalculateAngle(DateTime time)
        {
            TimeSpan timeOfDay = time.TimeOfDay;

            return CalculateAngle(timeOfDay);
        }

        private double CalculateAngle(TimeSpan time)
        {
            TimeSpan timeOfDay = time;

            double secondsFactor = (-timeOfDay.TotalSeconds)*(Math.PI/6)/3600;

            return -3*Math.PI/2 + secondsFactor;
        }

        private Point CalculatePosition(double θ)
        {
            double r = a + b*θ;

            double x = r*Math.Cos(θ) + x0;
            double y = r*Math.Sin(θ) + y0;

            return new Point(x, y);
        }

        private TransformGroup GetPointTransform(Size size, double zoom = 1)
        {
            double height = size.Height;
            double width = size.Width;

            if (height == 0.00 || width == 0.00) return new TransformGroup();

            double widthRatio = width/90.90;
            double heightRatio = height/100.00;
            double minRatio = Math.Min(widthRatio, heightRatio);
            double scale = 1.0;


            if (minRatio == widthRatio)
                scale = width/90.9;
            else
                scale = height/100;


            scale *= zoom;

            double horizontalTranslation = Math.Abs(width - scale*90.9)/2;
            double verticalTranslation = Math.Abs(height - scale*100)/2;

            var scaleTransform = new ScaleTransform(scale, scale);
            var translateTransform = new TranslateTransform(horizontalTranslation, verticalTranslation);

            var trg = new TransformGroup();
            trg.Children.Add(scaleTransform);
            trg.Children.Add(translateTransform);
            return trg;
        }

        private void Initialize()
        {
            _now.SetValue(Panel.ZIndexProperty, 10);
            _now.Height = _now.Width = 15;

            for (int i = 0; i <= 24; i++)
            {
                var ts = new TimeSpan(i, 0, 0);

                double angle = CalculateAngle(ts);

                var tb = new TextBlock {Style = HourLabelStyle, Text = i.ToString()};

                Point pos = CalculatePosition(angle);

                if (_canvas != null) _canvas.Children.Add(tb);

                _hourPositions.Add(pos);

                _hourLabels.Add(tb);
            }
        }

        private void SpiralItemsControl_LayoutUpdated(object sender, EventArgs e)
        {
            //ArrangeOverride(new Size(ActualWidth, ActualHeight));
        }

        private void TimerElapsed(object sender, ElapsedEventArgs e)
        {
            Dispatcher.Invoke((Action) (() => ArrangeNow(new Size(ActualWidth, ActualHeight))));
        }

        #endregion Methods 
    }
}